/*
 * Copyright (c) 1998, 2010, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.swing.text.html;

import java.io.Writer;
import java.io.IOException;
import java.util.*;
import java.awt.Color;
import javax.swing.text.*;

/**
 * MinimalHTMLWriter is a fallback writer used by the
 * HTMLEditorKit to write out HTML for a document that
 * is a not produced by the EditorKit.
 *
 * The format for the document is:
 * <pre>
 * &lt;html&gt;
 *   &lt;head&gt;
 *     &lt;style&gt;
 *        &lt;!-- list of named styles
 *         p.normal {
 *            font-family: SansSerif;
 *            margin-height: 0;
 *            font-size: 14
 *         }
 *        --&gt;
 *      &lt;/style&gt;
 *   &lt;/head&gt;
 *   &lt;body&gt;
 *    &lt;p style=normal&gt;
 *        <b>Bold, italic, and underline attributes
 *        of the run are emitted as HTML tags.
 *        The remaining attributes are emitted as
 *        part of the style attribute of a &lt;span&gt; tag.
 *        The syntax is similar to inline styles.</b>
 *    &lt;/p&gt;
 *   &lt;/body&gt;
 * &lt;/html&gt;
 * </pre>
 *
 * @author Sunita Mani
 */

public class MinimalHTMLWriter extends AbstractWriter {

  /**
   * These static finals are used to
   * tweak and query the fontMask about which
   * of these tags need to be generated or
   * terminated.
   */
  private static final int BOLD = 0x01;
  private static final int ITALIC = 0x02;
  private static final int UNDERLINE = 0x04;

  // Used to map StyleConstants to CSS.
  private static final CSS css = new CSS();

  private int fontMask = 0;

  int startOffset = 0;
  int endOffset = 0;

  /**
   * Stores the attributes of the previous run.
   * Used to compare with the current run's
   * attributeset.  If identical, then a
   * &lt;span&gt; tag is not emitted.
   */
  private AttributeSet fontAttributes;

  /**
   * Maps from style name as held by the Document, to the archived
   * style name (style name written out). These may differ.
   */
  private Hashtable<String, String> styleNameMapping;

  /**
   * Creates a new MinimalHTMLWriter.
   *
   * @param w Writer
   * @param doc StyledDocument
   */
  public MinimalHTMLWriter(Writer w, StyledDocument doc) {
    super(w, doc);
  }

  /**
   * Creates a new MinimalHTMLWriter.
   *
   * @param w Writer
   * @param doc StyledDocument
   * @param pos The location in the document to fetch the content.
   * @param len The amount to write out.
   */
  public MinimalHTMLWriter(Writer w, StyledDocument doc, int pos, int len) {
    super(w, doc, pos, len);
  }

  /**
   * Generates HTML output
   * from a StyledDocument.
   *
   * @throws IOException on any I/O error
   * @throws BadLocationException if pos represents an invalid location within the document.
   */
  public void write() throws IOException, BadLocationException {
    styleNameMapping = new Hashtable<String, String>();
    writeStartTag("<html>");
    writeHeader();
    writeBody();
    writeEndTag("</html>");
  }


  /**
   * Writes out all the attributes for the
   * following types:
   * StyleConstants.ParagraphConstants,
   * StyleConstants.CharacterConstants,
   * StyleConstants.FontConstants,
   * StyleConstants.ColorConstants.
   * The attribute name and value are separated by a colon.
   * Each pair is separated by a semicolon.
   *
   * @throws IOException on any I/O error
   */
  protected void writeAttributes(AttributeSet attr) throws IOException {
    Enumeration attributeNames = attr.getAttributeNames();
    while (attributeNames.hasMoreElements()) {
      Object name = attributeNames.nextElement();
      if ((name instanceof StyleConstants.ParagraphConstants) ||
          (name instanceof StyleConstants.CharacterConstants) ||
          (name instanceof StyleConstants.FontConstants) ||
          (name instanceof StyleConstants.ColorConstants)) {
        indent();
        write(name.toString());
        write(':');
        write(css.styleConstantsValueToCSSValue
            ((StyleConstants) name, attr.getAttribute(name)).
            toString());
        write(';');
        write(NEWLINE);
      }
    }
  }


  /**
   * Writes out text.
   *
   * @throws IOException on any I/O error
   */
  protected void text(Element elem) throws IOException, BadLocationException {
    String contentStr = getText(elem);
    if ((contentStr.length() > 0) &&
        (contentStr.charAt(contentStr.length() - 1) == NEWLINE)) {
      contentStr = contentStr.substring(0, contentStr.length() - 1);
    }
    if (contentStr.length() > 0) {
      write(contentStr);
    }
  }

  /**
   * Writes out a start tag appropriately
   * indented.  Also increments the indent level.
   *
   * @throws IOException on any I/O error
   */
  protected void writeStartTag(String tag) throws IOException {
    indent();
    write(tag);
    write(NEWLINE);
    incrIndent();
  }


  /**
   * Writes out an end tag appropriately
   * indented.  Also decrements the indent level.
   *
   * @throws IOException on any I/O error
   */
  protected void writeEndTag(String endTag) throws IOException {
    decrIndent();
    indent();
    write(endTag);
    write(NEWLINE);
  }


  /**
   * Writes out the &lt;head&gt; and &lt;style&gt;
   * tags, and then invokes writeStyles() to write
   * out all the named styles as the content of the
   * &lt;style&gt; tag.  The content is surrounded by
   * valid HTML comment markers to ensure that the
   * document is viewable in applications/browsers
   * that do not support the tag.
   *
   * @throws IOException on any I/O error
   */
  protected void writeHeader() throws IOException {
    writeStartTag("<head>");
    writeStartTag("<style>");
    writeStartTag("<!--");
    writeStyles();
    writeEndTag("-->");
    writeEndTag("</style>");
    writeEndTag("</head>");
  }


  /**
   * Writes out all the named styles as the
   * content of the &lt;style&gt; tag.
   *
   * @throws IOException on any I/O error
   */
  protected void writeStyles() throws IOException {
        /*
         *  Access to DefaultStyledDocument done to workaround
         *  a missing API in styled document to access the
         *  stylenames.
         */
    DefaultStyledDocument styledDoc = ((DefaultStyledDocument) getDocument());
    Enumeration styleNames = styledDoc.getStyleNames();

    while (styleNames.hasMoreElements()) {
      Style s = styledDoc.getStyle((String) styleNames.nextElement());

      /** PENDING: Once the name attribute is removed
       from the list we check check for 0. **/
      if (s.getAttributeCount() == 1 &&
          s.isDefined(StyleConstants.NameAttribute)) {
        continue;
      }
      indent();
      write("p." + addStyleName(s.getName()));
      write(" {\n");
      incrIndent();
      writeAttributes(s);
      decrIndent();
      indent();
      write("}\n");
    }
  }


  /**
   * Iterates over the elements in the document
   * and processes elements based on whether they are
   * branch elements or leaf elements.  This method specially handles
   * leaf elements that are text.
   *
   * @throws IOException on any I/O error
   */
  protected void writeBody() throws IOException, BadLocationException {
    ElementIterator it = getElementIterator();

        /*
          This will be a section element for a styled document.
          We represent this element in HTML as the body tags.
          Therefore we ignore it.
         */
    it.current();

    Element next;

    writeStartTag("<body>");

    boolean inContent = false;

    while ((next = it.next()) != null) {
      if (!inRange(next)) {
        continue;
      }
      if (next instanceof AbstractDocument.BranchElement) {
        if (inContent) {
          writeEndParagraph();
          inContent = false;
          fontMask = 0;
        }
        writeStartParagraph(next);
      } else if (isText(next)) {
        writeContent(next, !inContent);
        inContent = true;
      } else {
        writeLeaf(next);
        inContent = true;
      }
    }
    if (inContent) {
      writeEndParagraph();
    }
    writeEndTag("</body>");
  }


  /**
   * Emits an end tag for a &lt;p&gt;
   * tag.  Before writing out the tag, this method ensures
   * that all other tags that have been opened are
   * appropriately closed off.
   *
   * @throws IOException on any I/O error
   */
  protected void writeEndParagraph() throws IOException {
    writeEndMask(fontMask);
    if (inFontTag()) {
      endSpanTag();
    } else {
      write(NEWLINE);
    }
    writeEndTag("</p>");
  }


  /**
   * Emits the start tag for a paragraph. If
   * the paragraph has a named style associated with it,
   * then this method also generates a class attribute for the
   * &lt;p&gt; tag and sets its value to be the name of the
   * style.
   *
   * @throws IOException on any I/O error
   */
  protected void writeStartParagraph(Element elem) throws IOException {
    AttributeSet attr = elem.getAttributes();
    Object resolveAttr = attr.getAttribute(StyleConstants.ResolveAttribute);
    if (resolveAttr instanceof StyleContext.NamedStyle) {
      writeStartTag(
          "<p class=" + mapStyleName(((StyleContext.NamedStyle) resolveAttr).getName()) + ">");
    } else {
      writeStartTag("<p>");
    }
  }


  /**
   * Responsible for writing out other non-text leaf
   * elements.
   *
   * @throws IOException on any I/O error
   */
  protected void writeLeaf(Element elem) throws IOException {
    indent();
    if (elem.getName() == StyleConstants.IconElementName) {
      writeImage(elem);
    } else if (elem.getName() == StyleConstants.ComponentElementName) {
      writeComponent(elem);
    }
  }


  /**
   * Responsible for handling Icon Elements;
   * deliberately unimplemented.  How to implement this method is
   * an issue of policy.  For example, if you're generating
   * an &lt;img&gt; tag, how should you
   * represent the src attribute (the location of the image)?
   * In certain cases it could be a URL, in others it could
   * be read from a stream.
   *
   * @param elem element of type StyleConstants.IconElementName
   */
  protected void writeImage(Element elem) throws IOException {
  }


  /**
   * Responsible for handling Component Elements;
   * deliberately unimplemented.
   * How this method is implemented is a matter of policy.
   */
  protected void writeComponent(Element elem) throws IOException {
  }


  /**
   * Returns true if the element is a text element.
   */
  protected boolean isText(Element elem) {
    return (elem.getName() == AbstractDocument.ContentElementName);
  }


  /**
   * Writes out the attribute set
   * in an HTML-compliant manner.
   *
   * @throws IOException on any I/O error
   * @throws BadLocationException if pos represents an invalid location within the document.
   */
  protected void writeContent(Element elem, boolean needsIndenting)
      throws IOException, BadLocationException {

    AttributeSet attr = elem.getAttributes();
    writeNonHTMLAttributes(attr);
    if (needsIndenting) {
      indent();
    }
    writeHTMLTags(attr);
    text(elem);
  }


  /**
   * Generates
   * bold &lt;b&gt;, italic &lt;i&gt;, and &lt;u&gt; tags for the
   * text based on its attribute settings.
   *
   * @throws IOException on any I/O error
   */

  protected void writeHTMLTags(AttributeSet attr) throws IOException {

    int oldMask = fontMask;
    setFontMask(attr);

    int endMask = 0;
    int startMask = 0;
    if ((oldMask & BOLD) != 0) {
      if ((fontMask & BOLD) == 0) {
        endMask |= BOLD;
      }
    } else if ((fontMask & BOLD) != 0) {
      startMask |= BOLD;
    }

    if ((oldMask & ITALIC) != 0) {
      if ((fontMask & ITALIC) == 0) {
        endMask |= ITALIC;
      }
    } else if ((fontMask & ITALIC) != 0) {
      startMask |= ITALIC;
    }

    if ((oldMask & UNDERLINE) != 0) {
      if ((fontMask & UNDERLINE) == 0) {
        endMask |= UNDERLINE;
      }
    } else if ((fontMask & UNDERLINE) != 0) {
      startMask |= UNDERLINE;
    }
    writeEndMask(endMask);
    writeStartMask(startMask);
  }


  /**
   * Tweaks the appropriate bits of fontMask
   * to reflect whether the text is to be displayed in
   * bold, italic, and/or with an underline.
   */
  private void setFontMask(AttributeSet attr) {
    if (StyleConstants.isBold(attr)) {
      fontMask |= BOLD;
    }

    if (StyleConstants.isItalic(attr)) {
      fontMask |= ITALIC;
    }

    if (StyleConstants.isUnderline(attr)) {
      fontMask |= UNDERLINE;
    }
  }


  /**
   * Writes out start tags &lt;u&gt;, &lt;i&gt;, and &lt;b&gt; based on
   * the mask settings.
   *
   * @throws IOException on any I/O error
   */
  private void writeStartMask(int mask) throws IOException {
    if (mask != 0) {
      if ((mask & UNDERLINE) != 0) {
        write("<u>");
      }
      if ((mask & ITALIC) != 0) {
        write("<i>");
      }
      if ((mask & BOLD) != 0) {
        write("<b>");
      }
    }
  }

  /**
   * Writes out end tags for &lt;u&gt;, &lt;i&gt;, and &lt;b&gt; based on
   * the mask settings.
   *
   * @throws IOException on any I/O error
   */
  private void writeEndMask(int mask) throws IOException {
    if (mask != 0) {
      if ((mask & BOLD) != 0) {
        write("</b>");
      }
      if ((mask & ITALIC) != 0) {
        write("</i>");
      }
      if ((mask & UNDERLINE) != 0) {
        write("</u>");
      }
    }
  }


  /**
   * Writes out the remaining
   * character-level attributes (attributes other than bold,
   * italic, and underline) in an HTML-compliant way.  Given that
   * attributes such as font family and font size have no direct
   * mapping to HTML tags, a &lt;span&gt; tag is generated and its
   * style attribute is set to contain the list of remaining
   * attributes just like inline styles.
   *
   * @throws IOException on any I/O error
   */
  protected void writeNonHTMLAttributes(AttributeSet attr) throws IOException {

    String style = "";
    String separator = "; ";

    if (inFontTag() && fontAttributes.isEqual(attr)) {
      return;
    }

    boolean first = true;
    Color color = (Color) attr.getAttribute(StyleConstants.Foreground);
    if (color != null) {
      style += "color: " + css.styleConstantsValueToCSSValue
          ((StyleConstants) StyleConstants.Foreground,
              color);
      first = false;
    }
    Integer size = (Integer) attr.getAttribute(StyleConstants.FontSize);
    if (size != null) {
      if (!first) {
        style += separator;
      }
      style += "font-size: " + size.intValue() + "pt";
      first = false;
    }

    String family = (String) attr.getAttribute(StyleConstants.FontFamily);
    if (family != null) {
      if (!first) {
        style += separator;
      }
      style += "font-family: " + family;
      first = false;
    }

    if (style.length() > 0) {
      if (fontMask != 0) {
        writeEndMask(fontMask);
        fontMask = 0;
      }
      startSpanTag(style);
      fontAttributes = attr;
    } else if (fontAttributes != null) {
      writeEndMask(fontMask);
      fontMask = 0;
      endSpanTag();
    }
  }


  /**
   * Returns true if we are currently in a &lt;font&gt; tag.
   */
  protected boolean inFontTag() {
    return (fontAttributes != null);
  }

  /**
   * This is no longer used, instead &lt;span&gt; will be written out.
   * <p>
   * Writes out an end tag for the &lt;font&gt; tag.
   *
   * @throws IOException on any I/O error
   */
  protected void endFontTag() throws IOException {
    write(NEWLINE);
    writeEndTag("</font>");
    fontAttributes = null;
  }


  /**
   * This is no longer used, instead &lt;span&gt; will be written out.
   * <p>
   * Writes out a start tag for the &lt;font&gt; tag.
   * Because font tags cannot be nested,
   * this method closes out
   * any enclosing font tag before writing out a
   * new start tag.
   *
   * @throws IOException on any I/O error
   */
  protected void startFontTag(String style) throws IOException {
    boolean callIndent = false;
    if (inFontTag()) {
      endFontTag();
      callIndent = true;
    }
    writeStartTag("<font style=\"" + style + "\">");
    if (callIndent) {
      indent();
    }
  }

  /**
   * Writes out a start tag for the &lt;font&gt; tag.
   * Because font tags cannot be nested,
   * this method closes out
   * any enclosing font tag before writing out a
   * new start tag.
   *
   * @throws IOException on any I/O error
   */
  private void startSpanTag(String style) throws IOException {
    boolean callIndent = false;
    if (inFontTag()) {
      endSpanTag();
      callIndent = true;
    }
    writeStartTag("<span style=\"" + style + "\">");
    if (callIndent) {
      indent();
    }
  }

  /**
   * Writes out an end tag for the &lt;span&gt; tag.
   *
   * @throws IOException on any I/O error
   */
  private void endSpanTag() throws IOException {
    write(NEWLINE);
    writeEndTag("</span>");
    fontAttributes = null;
  }

  /**
   * Adds the style named <code>style</code> to the style mapping. This
   * returns the name that should be used when outputting. CSS does not
   * allow the full Unicode set to be used as a style name.
   */
  private String addStyleName(String style) {
    if (styleNameMapping == null) {
      return style;
    }
    StringBuilder sb = null;
    for (int counter = style.length() - 1; counter >= 0; counter--) {
      if (!isValidCharacter(style.charAt(counter))) {
        if (sb == null) {
          sb = new StringBuilder(style);
        }
        sb.setCharAt(counter, 'a');
      }
    }
    String mappedName = (sb != null) ? sb.toString() : style;
    while (styleNameMapping.get(mappedName) != null) {
      mappedName = mappedName + 'x';
    }
    styleNameMapping.put(style, mappedName);
    return mappedName;
  }

  /**
   * Returns the mapped style name corresponding to <code>style</code>.
   */
  private String mapStyleName(String style) {
    if (styleNameMapping == null) {
      return style;
    }
    String retValue = styleNameMapping.get(style);
    return (retValue == null) ? style : retValue;
  }

  private boolean isValidCharacter(char character) {
    return ((character >= 'a' && character <= 'z') ||
        (character >= 'A' && character <= 'Z'));
  }
}
